import tybc, IDE_vm
from tkinter import *
from tkinter.scrolledtext import *
from tkinter.filedialog import *
import time
import threading
from collections import deque

def command_new():
    w = root.focus_get()
    if isinstance(w, ScrolledText):
        w.delete("1.0", "end")
        w.edit_reset()

def command_load():
    w = root.focus_get()
    if isinstance(w, ScrolledText):
        fn = askopenfilename()
        if fn:
            with open(fn, encoding = 'utf-8') as f:
                src = f.read()
            w.delete("1.0", "end")
            w.update()
            time.sleep(.1)
            w.insert("1.0", src)
            w.mark_set("insert", '1.0')
            w.edit_reset()

def command_save():
    w = root.focus_get()
    if isinstance(w, ScrolledText):
        fn = asksaveasfilename()
        if fn:
            with open(fn, 'w', encoding = 'utf-8') as f:
                f.write(w.get("1.0", "end-1c"))

def command_compile():  
    src = txt_src.get("1.0", "end")
    try:
        code = tybc.compile_allinone(src)
    except Exception as e:
        codetxt = str(e)
    else:
        codetxt = '\n'.join(str(i) for i in code)
    txt_code.delete("1.0", "end")
    txt_code.update()
    time.sleep(.1)
    txt_code.insert("1.0", codetxt)
    txt_code.mark_set("insert", '1.0')
    txt_code.edit_reset()

def runvm(code, que_print, que_quit):
    try:
        IDE_vm.Vm(code, que_print, que_quit).run()
    except IDE_vm.CodeError as e:
        que_print.append("unknown instruction (line {}): {}".format(e.index+1, code[e.index]))

def stopvm():
    if thread_running and thread_running.is_alive():
        que_quit.append(0)
        thread_running.join()
        que_quit.clear()
    que_print.clear()

def command_run():
    global thread_running
    stopvm()
    code = txt_code.get("1.0", "end").rstrip().split('\n')
    txt_result.delete("1.0", "end")
    txt_result.edit_reset()
    txt_result.update()
    time.sleep(.1)
    thread_running = threading.Thread(target=runvm, args=(code, que_print, que_quit))
    thread_running.daemon = True
    thread_running.start()

def command_stop():
    stopvm()
    que_print.append('STOPPED')

def command_select_all():
    w = root.focus_get()
    if isinstance(w, ScrolledText):
        w.tag_add("sel", '1.0', 'end-1c')

def command_cut():
    w = root.focus_get()
    if isinstance(w, ScrolledText) and w.tag_ranges("sel"):
        t = w.get("sel.first", "sel.last")
        w.clipboard_clear()
        w.clipboard_append(t)
        w.delete("sel.first", "sel.last")

def command_copy():
    w = root.focus_get()
    if isinstance(w, ScrolledText) and w.tag_ranges("sel"):
        t = w.get("sel.first", "sel.last")
        w.clipboard_clear()
        w.clipboard_append(t)

def command_paste():
    w = root.focus_get()
    if isinstance(w, ScrolledText):
        try:
            t = w.clipboard_get()
        except TclError:
            set_message("clipboard is empty")
            return
        if w.tag_ranges("sel"):
            w.mark_set("insert", "sel.first")
            w.delete("sel.first", "sel.last")
        w.insert("insert", t)

def command_delete():
    w = root.focus_get()
    if isinstance(w, ScrolledText) and w.tag_ranges("sel"):
        w.delete("sel.first", "sel.last")

def command_undo():
    w = root.focus_get()
    if isinstance(w, ScrolledText) and w.cget('undo'):
        try:
            w.edit_undo()
        except TclError:
            set_message("undo stack is empty")
    else:
        set_message("cannot undo/redo in this widget")

def command_redo():
    w = root.focus_get()
    if isinstance(w, ScrolledText) and w.cget('undo'):
        try:
            w.edit_redo()
        except TclError:
            set_message("redo stack is empty")
    else:
        set_message("cannot undo/redo in this widget")
            
def command_quit():
    root.destroy()

def new_tab(event):
    w = event.widget
    if w.tag_ranges("sel"):
        w.mark_set("insert", "sel.first")
        w.delete("sel.first", "sel.last")
    w.insert("insert", " " * 4)
    return "break" # suppress Tab insertion

def switch_next(event):
    event.widget.next.focus_set()
    return 'break'
    
def switch_prev(event):
    event.widget.prev.focus_set()
    return 'break'

def print_result():
    try:
        s = que_print.popleft()
    except IndexError:
        pass
    else:
        txt_result.insert("end", str(s) + '\n')
        txt_result.see("end")
        txt_result.update()
    root.after(result_interval, print_result)

def set_statusbar():
    lmt = label_message.timer
    if lmt == 1:
        label_message.timer -= 1
        label_message.config(text = '')
    elif lmt > 1:
        label_message.timer -= 1
    sl, sc = txt_src.index("insert").split(".")
    cl, cc = txt_code.index("insert").split(".")
    rl, rc = txt_result.index("insert").split(".")
    txt = "{}:{}  {}:{}  {}:{}".format(sl, int(sc)+1, cl, int(cc)+1, rl, int(rc)+1)
    label_cursorpos.config(text = txt)
    root.after(statusbar_refresh, set_statusbar)

def set_message(t):
    label_message.config(text = t)
    label_message.timer = 3 * 1000 // statusbar_refresh

thread_running = None
que_print = deque()
que_quit = deque()
result_interval = 100
statusbar_refresh = 100

root = Tk()
root.title('IDE')

menubar = Menu(root)
root.config(menu = menubar)

menu_file = Menu(menubar, tearoff = 0)
menu_file.add_command(label = 'New', command = command_new)
menu_file.add_command(label = 'Load', command = command_load)
menu_file.add_command(label = 'Save', command = command_save)
menu_file.add_separator()
menu_file.add_command(label = 'Quit', command = command_quit)
menubar.add_cascade(label = 'File', menu = menu_file, underline = 0)

menu_edit = Menu(menubar, tearoff = 0)
menu_edit.add_command(label = 'Undo', command = command_undo)
menu_edit.add_command(label = 'Redo', command = command_redo)
menu_edit.add_separator()
menu_edit.add_command(label = 'Cut', command = command_cut)
menu_edit.add_command(label = 'Copy', command = command_copy)
menu_edit.add_command(label = 'Paste', command = command_paste)
menu_edit.add_command(label = 'Delete', command = command_delete)
menu_edit.add_command(label = 'Select All', command = command_select_all)
menubar.add_cascade(label = 'Edit', menu = menu_edit, underline = 0)

menu_run = Menu(menubar, tearoff = 0)
menu_run.add_command(label = 'Compile', command = command_compile)
menu_run.add_command(label = 'Run/Restart', command = command_run)
menu_run.add_command(label = 'Stop', command = command_stop)
menubar.add_cascade(label = 'Run', menu = menu_run, underline = 0)

toolbar = Frame(root)
toolbar.pack(fill = "x")

btn_cut = Button(toolbar, text = "cut", command = command_cut)
btn_cut.pack(side = "left")

btn_copy = Button(toolbar, text = "copy", command = command_copy)
btn_copy.pack(side = "left")

btn_paste = Button(toolbar, text = "paste", command = command_paste)
btn_paste.pack(side = "left")

btn_delete = Button(toolbar, text = "delete", command = command_delete)
btn_delete.pack(side = "left")

btn_select_all = Button(toolbar, text = "select all", command = command_select_all)
btn_select_all.pack(side = "left")

padding = Frame(toolbar, width = 5)
padding.pack(side = "left")

btn_undo = Button(toolbar, text = "undo", command = command_undo)
btn_undo.pack(side = "left")

btn_redo = Button(toolbar, text = "redo", command = command_redo)
btn_redo.pack(side = "left")

pw = PanedWindow(root, orient = "vertical", sashwidth = 4)
pw.pack(fill = 'both', expand = True)

txt_src = ScrolledText(pw, height = 20, bg = "#ffe", undo = 1, maxundo = 1000)
pw.add(txt_src)
txt_src.bind('<Tab>', new_tab)
txt_src.bind('<Shift-Tab>', new_tab)
txt_src.bind('<Control-Tab>', switch_next)
txt_src.bind('<Control-Shift-Tab>', switch_prev)
try: # Shift-Tab on Linux
    txt_src.bind('<ISO_Left_Tab>', new_tab)
    txt_src.bind('<Control-ISO_Left_Tab>', switch_prev)
except TclError:
    pass

txt_code = ScrolledText(pw, height = 8, bg = "#fef", undo = 1, maxundo = 1000)
pw.add(txt_code)
txt_code.bind('<Tab>', new_tab)
txt_code.bind('<Shift-Tab>', new_tab)
txt_code.bind('<Control-Tab>', switch_next)
txt_code.bind('<Control-Shift-Tab>', switch_prev)
try:
    txt_code.bind('<ISO_Left_Tab>', new_tab)
    txt_code.bind('<Control-ISO_Left_Tab>', switch_prev)
except TclError:
    pass

txt_result = ScrolledText(pw, height = 8, bg = "#eff", undo = 1, maxundo = 1000)
pw.add(txt_result)
txt_result.bind('<Tab>', new_tab)
txt_result.bind('<Shift-Tab>', new_tab)
txt_result.bind('<Control-Tab>', switch_next)
txt_result.bind('<Control-Shift-Tab>', switch_prev)
try:
    txt_result.bind('<ISO_Left_Tab>', new_tab)
    txt_result.bind('<Control-ISO_Left_Tab>', switch_prev)
except TclError:
    pass

txts = [txt_src, txt_code, txt_result]
for i, t in enumerate(txts):
    t.next = txts[(i+1) % len(txts)]
    t.prev = txts[(i-1) % len(txts)]

statusbar = Frame(root)
statusbar.pack(fill = 'x')

label_message = Label(statusbar)
label_message.pack(side = 'left')
label_message.timer = 0

label_cursorpos = Label(statusbar)
label_cursorpos.pack(side = 'right')

print_result()
set_statusbar()
txt_src.focus_set()
root.mainloop()
